Skip to content

Conversation

@LeeCh0129
Copy link
Contributor

@LeeCh0129 LeeCh0129 commented Jul 29, 2025

📌 변경 사항 개요

예약 내역 페이지 + 후기 작성을 구현 완료했습니다. 무한스크롤 + 상태별 필터링 + API 연동까지 완료했고, UI는 Desktop만 우선적으로 해서 아직 태블릿/모바일 반응형 구현은 남은 상태입니다.

📝 상세 내용

상태별 필터링

  • 전체/예약신청/예약승인/예약거절/예약취소/체험완료 필터 지원
  • 공통 드롭다운 컴포넌트를 사용하고, 금일 스크럼에서 스크롤바 없이 전체 목록을 보여주기로 결정되어 dsiableScroll 옵션 추가
  • 기획 요구사항과 API가 상이하여 API에 맞춰 예약 취소는 pending 상태에서만 취소 가능하도록 했습니다.
Axios 에러 상세: {
  status: 400,
  message: '예약 취소는 예약 신청 상태에서만 가능합니다.',
  responseData: { message: '예약 취소는 예약 신청 상태에서만 가능합니다.' },
}

무한 스크롤

  • 리액트 쿼리 useInfiniteQuery + Intersection Obeserver 조합

API 연동

  • Next.js API Routes로 프록시 패턴(/api/reservations)

🔗 관련 이슈

🖼️ 스크린샷(선택사항)

default.mov
image

💡 참고 사항

  • 올려둔 동영상내에서 이미지가 보이지 않는 카드는 스웨거에서 더미데이터를 만들때 이미지 url을 추가하지 않아 이미지가 보이지 않는 상태입니다.
  • UI 데스크탑까지 완성, 태블릿/모바일은 추후 구현 예정

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 내 예약 내역 페이지가 새롭게 구현되어 예약 목록 조회, 상태별 필터링, 무한 스크롤, 예약 취소 및 확인 모달, 후기 작성 모달, 빈 상태 안내 등을 지원합니다.
    • 예약 카드에서 예약 상세 정보, 상태, 취소 및 후기 작성 버튼을 제공합니다.
    • 예약 필터 드롭다운 및 스크롤 비활성화 옵션이 추가되었습니다.
    • 예약 취소 확인 모달과 후기 작성 모달이 추가되었습니다.
    • 별점 평가 컴포넌트와 빈 문서 및 빈 별 아이콘 SVG 컴포넌트가 추가되었습니다.
    • 예약 관련 API 연동, 타입 정의, 쿼리 훅 및 무한 스크롤 커스텀 훅이 도입되었습니다.
    • 예약 취소 및 후기 생성 API 라우트 핸들러가 추가되었습니다.
  • 버그 수정

    • 해당 없음.
  • 문서화

    • 해당 없음.
  • 리팩터링/스타일

    • 해당 없음.

@LeeCh0129 LeeCh0129 self-assigned this Jul 29, 2025
@LeeCh0129 LeeCh0129 added the ✨ Feature 기능구현 label Jul 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 29, 2025

Walkthrough

예약 내역 페이지 기능이 새롭게 구현되었습니다. 예약 내역 조회, 필터링, 무한 스크롤, 예약 취소, 후기 작성, 빈 상태 표시 등 예약 관련 주요 컴포넌트와 API, 타입, 훅, 상수 파일이 대거 추가되었습니다. API 연동 및 상태 관리, 모달 UI, 드롭다운 등도 포함되어 있습니다.

Changes

Cohort / File(s) Change Summary
예약 내역 페이지 UI 및 컴포넌트
src/app/(with-header)/mypage/reservations/page.tsx,
src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx,
src/app/(with-header)/mypage/reservations/components/ReservationFilter.tsx,
src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx,
src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx,
src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx
예약 내역 페이지 전체 레이아웃, 예약 카드, 필터, 빈 상태, 예약 취소 모달, 후기 작성 모달 등 주요 컴포넌트 신규 구현
예약 API 및 백엔드 연동
src/apis/reservations.ts,
src/app/api/reservations/route.ts,
src/app/api/reservations/[id]/route.ts,
src/app/api/reservations/[id]/reviews/route.ts
예약 내역 조회, 예약 취소, 후기 작성 API 모듈 및 Next.js API 라우트 신규 구현
예약 관련 타입/상수
src/types/reservationTypes.ts,
src/constants/reservationConstants.ts
예약 상태, 예약 엔티티, 필터 옵션 등 타입 및 상수 정의
예약 관련 커스텀 훅
src/hooks/useReservationQueries.ts,
src/hooks/useInfiniteScroll.ts
예약 내역 쿼리, 예약 취소, 후기 작성, 무한 스크롤용 커스텀 훅 신규 구현
공통 컴포넌트/타입 개선
src/components/Dropdown.tsx,
src/types/dropdownTypes.ts
드롭다운 컴포넌트에 스크롤 비활성화 옵션 추가 및 타입 정의
아이콘 에셋
public/assets/svg/empty-document.tsx,
public/assets/svg/star-empty.tsx
빈 문서 및 빈 별 아이콘 SVG 컴포넌트 추가
별점 컴포넌트
src/components/Rating.tsx
별점 선택 UI 컴포넌트 신규 구현

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MyReservationsPage
    participant useMyReservations
    participant API_Route
    participant Backend

    User->>MyReservationsPage: 페이지 접속/필터 변경/스크롤
    MyReservationsPage->>useMyReservations: 예약 내역 요청
    useMyReservations->>API_Route: /api/reservations?status=... GET
    API_Route->>Backend: /my-reservations?status=... GET
    Backend-->>API_Route: 예약 내역 데이터 반환
    API_Route-->>useMyReservations: 예약 내역 데이터 반환
    useMyReservations-->>MyReservationsPage: 예약 내역 데이터 반환
    MyReservationsPage->>User: 예약 카드/빈 상태 등 렌더링

    User->>MyReservationsPage: 예약 취소 버튼 클릭
    MyReservationsPage->>CancelReservationModal: 모달 오픈
    User->>CancelReservationModal: 취소 확인
    CancelReservationModal->>useCancelReservation: 예약 취소 요청
    useCancelReservation->>API_Route: /api/reservations/[id] PATCH
    API_Route->>Backend: /my-reservations/[id] PATCH
    Backend-->>API_Route: 취소 결과 반환
    API_Route-->>useCancelReservation: 취소 결과 반환
    useCancelReservation-->>MyReservationsPage: 쿼리 invalidate 및 재요청

    User->>MyReservationsPage: 후기 작성 버튼 클릭
    MyReservationsPage->>ReviewModal: 모달 오픈
    User->>ReviewModal: 후기 작성 및 제출
    ReviewModal->>useCreateReview: 후기 작성 요청
    useCreateReview->>API_Route: /api/reservations/[id]/reviews POST
    API_Route->>Backend: 후기 저장 요청
    Backend-->>API_Route: 후기 저장 결과 반환
    API_Route-->>useCreateReview: 후기 저장 결과 반환
    useCreateReview-->>MyReservationsPage: 쿼리 invalidate 및 재요청
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective (Issue) Addressed Explanation
예약 내역 페이지 레이아웃 (#71)
예약 내역 API 연결 (#80)
후기 작성 컴포넌트 및 API연동 (#98)

Assessment against linked issues: Out-of-scope changes

(해당 이슈의 목적과 무관한 변경 사항은 발견되지 않았습니다.)

Suggested labels

🎨 design, 📬 API

Suggested reviewers

  • minimo-9
  • BokyungCodes

Poem

🐇
예약 내역 한가득, 카드가 줄을 서네
취소도 필터도, 모달이 척척!
빈 페이지엔 귀여운 아이콘,
무한 스크롤로 쭉쭉 이어져
오늘도 토끼는 코드를 춤추네!
🗂️✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/71

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0e2789 and 9fd63c0.

📒 Files selected for processing (15)
  • public/assets/svg/empty-document.tsx (1 hunks)
  • src/apis/reservations.ts (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/ReservationFilter.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/page.tsx (1 hunks)
  • src/app/api/reservations/[id]/route.ts (1 hunks)
  • src/app/api/reservations/route.ts (1 hunks)
  • src/components/Dropdown.tsx (2 hunks)
  • src/constants/reservationConstants.ts (1 hunks)
  • src/hooks/useInfiniteScroll.ts (1 hunks)
  • src/hooks/useReservationQueries.ts (1 hunks)
  • src/types/dropdownTypes.ts (2 hunks)
  • src/types/reservationTypes.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/types/reservationTypes.ts (4)
src/types/bookingStoreTypes.ts (1)
  • BookingState (1-23)
src/types/activityDetailType.ts (2)
  • ActivitySchedule (22-27)
  • ActivityDetail (46-62)
src/app/(with-header)/myactivity/components/ReservationForm.tsx (1)
  • DateSlot (14-18)
src/components/FloatingBox/BookingInterface.tsx (1)
  • BookingInterface (17-157)
src/constants/reservationConstants.ts (1)
src/types/reservationTypes.ts (1)
  • ReservationStatus (2-7)
🪛 Biome (2.1.2)
public/assets/svg/empty-document.tsx

[error] 4-11: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🔇 Additional comments (15)
src/hooks/useInfiniteScroll.ts (1)

19-46: 훌륭한 무한 스크롤 구현입니다!

Intersection Observer API를 올바르게 사용하여 성능 최적화된 무한 스크롤을 구현했습니다. useCallback과 useRef를 적절히 활용하여 불필요한 리렌더링을 방지하고 중복 요청도 차단했습니다.

src/constants/reservationConstants.ts (1)

1-38: 예약 상태 관리를 위한 잘 구조화된 상수 정의입니다.

TypeScript 타입을 적절히 활용하고, readonly 배열과 const assertion을 사용하여 타입 안전성을 확보했습니다. 필터링과 표시를 위한 상수들이 명확하게 분리되어 있고, 한국어 라벨도 일관성 있게 정의되었습니다.

src/types/dropdownTypes.ts (1)

15-15: 드롭다운 컴포넌트의 유연성을 높이는 좋은 추가입니다.

disableScroll 속성을 선택적으로 추가하여 드롭다운의 스크롤 동작을 제어할 수 있게 했습니다. 기존 인터페이스와 일관성을 유지하며 후방 호환성도 보장됩니다.

Also applies to: 24-24

src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx (1)

3-17: 명확하고 사용자 친화적인 빈 상태 컴포넌트입니다.

EmptyDocumentIcon을 적절히 활용하여 시각적 피드백을 제공하고, 명확한 한국어 메시지로 사용자에게 현재 상태를 알려줍니다. 레이아웃과 스타일링도 깔끔하게 구현되었습니다.

src/components/Dropdown.tsx (1)

36-36: 새로운 disableScroll prop 구현이 잘 되었습니다.

예약 필터에서 요구사항에 맞게 스크롤을 비활성화할 수 있도록 하는 기능이 깔끔하게 구현되었습니다. 기본값 설정과 조건부 클래스 적용이 적절합니다.

Also applies to: 164-170

src/app/api/reservations/[id]/route.ts (1)

42-56: 에러 처리 구조가 잘 구현되었습니다.

Axios 에러와 일반 에러를 적절히 구분하여 처리하고 있으며, 한국어 에러 메시지도 일관성 있게 작성되었습니다.

src/app/(with-header)/mypage/reservations/components/ReservationFilter.tsx (1)

32-42: 컴포넌트 구조와 Dropdown 활용이 적절합니다.

상수를 활용한 옵션 매핑과 disableScroll prop 사용이 요구사항에 맞게 잘 구현되었습니다.

src/app/api/reservations/route.ts (1)

35-45: API 요청 구조와 응답 처리가 잘 구현되었습니다.

쿼리 파라미터 구성과 인증 헤더 설정이 적절하며, 백엔드 응답을 그대로 전달하는 프록시 패턴이 잘 적용되었습니다.

src/hooks/useReservationQueries.ts (1)

17-34: 무한 스크롤 구현이 React Query 모범 사례를 잘 따르고 있습니다.

useInfiniteQuery 사용법과 캐시 관리가 적절하며, 쿼리 키 구조도 잘 설계되었습니다.

src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx (1)

14-65: 전체적으로 잘 구현된 컴포넌트입니다

모달 컴포넌트가 적절한 상태 관리, 로딩 처리, 접근성을 고려하여 잘 구현되었습니다.

src/apis/reservations.ts (1)

1-47: API 함수가 잘 구성되었습니다

타입 정의, 문서화, 그리고 일관된 API 인터페이스가 적절히 구현되었습니다.

src/app/(with-header)/mypage/reservations/page.tsx (1)

1-165: 전체적으로 잘 구현된 예약 내역 페이지입니다

무한 스크롤, 필터링, 상태 관리가 적절히 구현되었습니다. 위의 개선사항들을 반영하면 더욱 완성도 높은 코드가 될 것입니다.

src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (1)

21-107: 잘 구조화된 예약 카드 컴포넌트입니다

상태별 조건부 렌더링, 이미지 최적화, 명확한 UI 구조가 잘 구현되었습니다.

src/types/reservationTypes.ts (2)

39-41: 업데이트 요청 타입의 제한적 구현

현재 UpdateReservationRequest는 'canceled' 상태로만 변경 가능하도록 제한되어 있습니다. PR 설명에 따르면 의도된 것으로 보이나, 향후 다른 상태 변경이 필요할 경우를 대비한 확장성을 고려해보세요.

다른 상태로의 변경이 필요한 비즈니스 요구사항이 있는지 확인해 주세요.


1-48: 타입 정의가 명확하고 잘 구성되었습니다

예약 시스템에 필요한 모든 타입이 체계적으로 정의되어 있으며, TypeScript의 장점을 잘 활용하고 있습니다.

@@ -0,0 +1,23 @@
import React from 'react';

const EmptyDocumentIcon = ({ size = 24, ...props }) => (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

TypeScript 타입 정의가 필요합니다.

props에 대한 TypeScript 타입이 정의되지 않았습니다. 타입 안전성을 위해 인터페이스를 추가해주세요.

+interface EmptyDocumentIconProps {
+  size?: number;
+  [key: string]: any;
+}

-const EmptyDocumentIcon = ({ size = 24, ...props }) => (
+const EmptyDocumentIcon = ({ size = 24, ...props }: EmptyDocumentIconProps) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const EmptyDocumentIcon = ({ size = 24, ...props }) => (
interface EmptyDocumentIconProps {
size?: number;
[key: string]: any;
}
const EmptyDocumentIcon = ({ size = 24, ...props }: EmptyDocumentIconProps) => (
🤖 Prompt for AI Agents
In public/assets/svg/empty-document.tsx at line 3, the component
EmptyDocumentIcon lacks TypeScript type definitions for its props. Define an
interface specifying the expected prop types, including the optional size as a
number and any other props, then apply this interface to the component's props
parameter to ensure type safety.

Comment on lines +4 to +11
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 131 178'
{...props}
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

접근성을 위한 대체 텍스트가 필요합니다.

스크린 리더 사용자를 위해 SVG에 대체 텍스트를 제공해야 합니다. title 요소나 aria-label 속성을 추가해주세요.

  <svg
    xmlns='http://www.w3.org/2000/svg'
    width={size}
    height={size}
    fill='none'
    viewBox='0 0 131 178'
+   role='img'
+   aria-label='빈 문서 아이콘'
    {...props}
  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 131 178'
{...props}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 131 178'
role='img'
aria-label='빈 문서 아이콘'
{...props}
>
🧰 Tools
🪛 Biome (2.1.2)

[error] 4-11: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In public/assets/svg/empty-document.tsx around lines 4 to 11, the SVG element
lacks accessible alternative text for screen readers. Add a <title> element
inside the SVG with a descriptive text or include an aria-label attribute on the
SVG element to provide meaningful alternative text for accessibility.

Comment on lines +16 to +27
const queryParams = new URLSearchParams();

if (params.cursorId) {
queryParams.append('cursorId', params.cursorId.toString());
}
if (params.size) {
queryParams.append('size', params.size.toString());
}
if (params.status) {
queryParams.append('status', params.status);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

쿼리 파라미터 생성 로직 간소화 가능

조건문을 제거하고 더 간결하게 작성할 수 있습니다.

-  const queryParams = new URLSearchParams();
-
-  if (params.cursorId) {
-    queryParams.append('cursorId', params.cursorId.toString());
-  }
-  if (params.size) {
-    queryParams.append('size', params.size.toString());
-  }
-  if (params.status) {
-    queryParams.append('status', params.status);
-  }
+  const queryParams = new URLSearchParams(
+    Object.entries(params)
+      .filter(([_, value]) => value !== undefined)
+      .map(([key, value]) => [key, String(value)])
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const queryParams = new URLSearchParams();
if (params.cursorId) {
queryParams.append('cursorId', params.cursorId.toString());
}
if (params.size) {
queryParams.append('size', params.size.toString());
}
if (params.status) {
queryParams.append('status', params.status);
}
const queryParams = new URLSearchParams(
Object.entries(params)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => [key, String(value)])
);
🤖 Prompt for AI Agents
In src/apis/reservations.ts around lines 16 to 27, the query parameter appending
uses multiple if conditions to check each parameter before appending. Simplify
this by iterating over the params object keys and appending only those with
defined values, removing explicit if statements to make the code more concise
and maintainable.

Comment on lines 22 to 30
<Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0'>
<div
className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16'
style={{
borderRadius: '12px',
background: '#FFFFFF',
boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)',
overflow: 'hidden',
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

중복된 스타일링 방식 개선 필요

classNamestyle 속성에서 동일한 스타일(borderRadius, background, boxShadow)이 중복 정의되어 있습니다. Tailwind 클래스를 사용하거나 style 객체 중 하나만 사용하는 것이 좋습니다.

-      <Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0'>
-        <div
-          className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16'
-          style={{
-            borderRadius: '12px',
-            background: '#FFFFFF',
-            boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)',
-            overflow: 'hidden',
-          }}
-        >
+      <Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'>
+        <div className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16'>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0'>
<div
className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16'
style={{
borderRadius: '12px',
background: '#FFFFFF',
boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)',
overflow: 'hidden',
}}
<Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'>
<div className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16'>
🤖 Prompt for AI Agents
In
src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx
around lines 22 to 30, the styles borderRadius, background, and boxShadow are
redundantly defined both in the className and the style attribute. To fix this,
consolidate the styling by either converting all these styles into appropriate
Tailwind CSS classes or by removing them from className and keeping them only in
the style object, ensuring no duplication and consistent styling approach.

Comment on lines +26 to +28
const body = await request.json();
const resolvedParams = await params;
const reservationId = resolvedParams.id;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

입력 데이터 검증을 추가하는 것을 고려해보세요.

요청 본문을 직접 백엔드로 전달하고 있습니다. 보안을 위해 허용되는 필드만 포함하도록 입력 데이터를 검증하는 것이 좋겠습니다.

const body = await request.json();
+
+// 허용되는 필드만 전달
+const allowedFields = ['status'];
+const validatedBody = Object.keys(body)
+  .filter(key => allowedFields.includes(key))
+  .reduce((obj, key) => {
+    obj[key] = body[key];
+    return obj;
+  }, {} as Record<string, any>);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = await request.json();
const resolvedParams = await params;
const reservationId = resolvedParams.id;
const body = await request.json();
// 허용되는 필드만 전달
const allowedFields = ['status'];
const validatedBody = Object.keys(body)
.filter(key => allowedFields.includes(key))
.reduce((obj, key) => {
obj[key] = body[key];
return obj;
}, {} as Record<string, any>);
const resolvedParams = await params;
const reservationId = resolvedParams.id;
🤖 Prompt for AI Agents
In src/app/api/reservations/[id]/route.ts around lines 26 to 28, the request
body is directly used without validation, which poses a security risk. Add input
validation to ensure only allowed fields are accepted from the request body
before processing or storing the data. Implement a whitelist of expected fields
and reject or sanitize any unexpected or malicious input.

// URL에서 쿼리 파라미터 추출
const { searchParams } = new URL(request.url);
const cursorId = searchParams.get('cursorId');
const size = searchParams.get('size') || '10';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

size 파라미터에 대한 검증을 추가하는 것을 고려해보세요.

현재 클라이언트에서 전달하는 size 값을 그대로 사용하고 있습니다. 매우 큰 값으로 인한 성능 문제를 방지하기 위해 최대값 제한을 두는 것이 좋겠습니다.

-const size = searchParams.get('size') || '10';
+const sizeParam = searchParams.get('size') || '10';
+const size = Math.min(parseInt(sizeParam, 10) || 10, 100).toString();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const size = searchParams.get('size') || '10';
const sizeParam = searchParams.get('size') || '10';
const size = Math.min(parseInt(sizeParam, 10) || 10, 100).toString();
🤖 Prompt for AI Agents
In src/app/api/reservations/route.ts at line 26, the 'size' parameter from
searchParams is used directly without validation, which can lead to performance
issues if a very large value is provided. Add validation to parse 'size' as an
integer, set a maximum allowed value (e.g., 100), and default to a safe number
if parsing fails or the value exceeds the maximum. This ensures the 'size'
parameter is always within acceptable limits.


const lastElementRef = useCallback(
(node: HTMLDivElement | null) => {
// 로딩중이거나 다음 페이지를 가져오는 중이면 중복요처 방지
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

한국어 주석의 오타를 수정해주세요.

"중복요처"를 "중복요청"으로 수정해주세요.

-      // 로딩중이거나 다음 페이지를 가져오는 중이면 중복요처 방지
+      // 로딩중이거나 다음 페이지를 가져오는 중이면 중복요청 방지
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 로딩중이거나 다음 페이지를 가져오는 중이면 중복요처 방지
// 로딩중이거나 다음 페이지를 가져오는 중이면 중복요청 방지
🤖 Prompt for AI Agents
In src/hooks/useInfiniteScroll.ts at line 29, there is a Korean comment with a
typo: "중복요처" should be corrected to "중복요청". Update the comment to fix this
spelling mistake.

Comment on lines +25 to +30
getNextPageParam: (lastPage) => {
// 다음 페이지가 있으면 cursorId 반환, 없으면 undefined
return lastPage.reservations.length === 10
? lastPage.cursorId
: undefined;
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

다음 페이지 판단 로직을 개선해보세요.

현재 로직은 정확히 10개의 항목이 반환될 때만 다음 페이지가 있다고 가정합니다. 마지막 페이지에서도 정확히 10개가 반환될 수 있는 경우를 고려해보세요.

더 안전한 방법은 백엔드에서 hasNext 플래그나 totalCount 같은 메타데이터를 제공하는 것입니다.

getNextPageParam: (lastPage) => {
- // 다음 페이지가 있으면 cursorId 반환, 없으면 undefined
- return lastPage.reservations.length === 10
-   ? lastPage.cursorId
-   : undefined;
+ // 백엔드에서 hasNext 플래그를 제공한다면
+ return lastPage.hasNext ? lastPage.cursorId : undefined;
},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getNextPageParam: (lastPage) => {
// 다음 페이지가 있으면 cursorId 반환, 없으면 undefined
return lastPage.reservations.length === 10
? lastPage.cursorId
: undefined;
},
getNextPageParam: (lastPage) => {
// 백엔드에서 hasNext 플래그를 제공한다면
return lastPage.hasNext ? lastPage.cursorId : undefined;
},
🤖 Prompt for AI Agents
In src/hooks/useReservationQueries.ts around lines 25 to 30, the current logic
assumes there is a next page only if exactly 10 reservations are returned, which
can be inaccurate if the last page also contains 10 items. To fix this, update
the pagination logic to rely on a more reliable indicator such as a `hasNext`
flag or `totalCount` metadata provided by the backend. Modify the code to check
this flag or compare the current count against the total count to determine if
there is a next page, instead of relying solely on the number of items returned.

Comment on lines +48 to +52
alert('예약이 취소되었습니다.');
},
onError: (error) => {
alert(`예약 취소 실패: ${error.message}`);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

사용자 경험을 위해 에러 처리 방식을 개선하는 것을 고려해보세요.

현재 alert를 사용하고 있지만, 토스트 메시지나 모달과 같은 더 나은 UX 패턴을 사용하는 것이 좋겠습니다.

onSuccess: () => {
  queryClient.invalidateQueries({
    queryKey: RESERVATION_QUERY_KEYS.RESERVATIONS,
  });
- alert('예약이 취소되었습니다.');
+ // 토스트 메시지 라이브러리 사용 예시
+ toast.success('예약이 취소되었습니다.');
},
onError: (error) => {
- alert(`예약 취소 실패: ${error.message}`);
+ toast.error(`예약 취소 실패: ${error.message}`);
},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
alert('예약이 취소되었습니다.');
},
onError: (error) => {
alert(`예약 취소 실패: ${error.message}`);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: RESERVATION_QUERY_KEYS.RESERVATIONS,
});
// 토스트 메시지 라이브러리 사용 예시
toast.success('예약이 취소되었습니다.');
},
onError: (error) => {
toast.error(`예약 취소 실패: ${error.message}`);
},
🤖 Prompt for AI Agents
In src/hooks/useReservationQueries.ts around lines 48 to 52, replace the alert
calls in the onError handler with a toast notification or modal component to
improve user experience. Remove the alert usage and integrate a toast or modal
library to display error messages in a less intrusive and more user-friendly
manner.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (2)

15-19: 타임존 처리가 개선되었습니다

이전 리뷰 코멘트를 반영하여 +09:00 타임존이 명시적으로 추가되었습니다. 이는 서버-클라이언트 간 시간 불일치 문제를 해결합니다.


43-43: 반응형 디자인 문제가 여전히 존재합니다

고정 너비 w-[792px]가 여전히 사용되고 있어 작은 화면에서 레이아웃 문제가 발생할 수 있습니다.

-    <div className='flex h-[204px] w-[792px] overflow-hidden rounded-[24px] border border-gray-300 bg-white'>
+    <div className='flex h-[204px] w-full max-w-[792px] overflow-hidden rounded-[24px] border border-gray-300 bg-white'>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 624e574 and 87b488d.

📒 Files selected for processing (1)
  • src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (1 hunks)
🔇 Additional comments (5)
src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (5)

1-8: imports와 기본 설정 검토 완료

필요한 모듈들이 적절히 임포트되었고, 타입 정의와 상수들도 올바르게 가져왔습니다.


9-13: 인터페이스 정의가 명확합니다

ReservationCardProps 인터페이스가 명확하게 정의되어 있고, 선택적 콜백 함수들도 적절히 타입이 지정되었습니다.


21-40: 컴포넌트 로직과 조건부 렌더링 검토

구조분해할당과 조건부 로직이 명확하게 구현되었습니다. isCompleted, showCancelButton, showReviewButton 변수들이 적절히 계산되고 있습니다.


44-52: 이미지 영역 구현이 적절합니다

Next.js의 Image 컴포넌트를 사용하여 최적화된 이미지 로딩을 구현했고, fill 속성과 object-cover를 통해 적절한 이미지 표시를 보장합니다.


54-73: 콘텐츠 영역 레이아웃이 잘 구성되었습니다

상태 라벨, 제목, 날짜/인원 정보가 적절한 간격과 스타일링으로 배치되었습니다. STATUS_COLORSSTATUS_LABELS 상수를 사용한 것도 일관성 있는 UI를 위해 좋은 접근입니다.

Comment on lines 46 to 51
<Image
src={activity.bannerImageUrl}
alt={activity.title}
fill
className='rounded-l-[24px] object-cover'
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

이미지 오류 처리 개선 필요

이미지 로딩 실패 시에 대한 처리가 없습니다. 사용자 경험 향상을 위해 오류 처리를 추가하는 것이 좋습니다.

        <Image
          src={activity.bannerImageUrl}
          alt={activity.title}
          fill
          className='rounded-l-[24px] object-cover'
+         onError={(e) => {
+           e.currentTarget.src = '/images/placeholder.jpg'; // 기본 이미지로 대체
+         }}
        />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Image
src={activity.bannerImageUrl}
alt={activity.title}
fill
className='rounded-l-[24px] object-cover'
/>
<Image
src={activity.bannerImageUrl}
alt={activity.title}
fill
className='rounded-l-[24px] object-cover'
onError={(e) => {
e.currentTarget.src = '/images/placeholder.jpg'; // 기본 이미지로 대체
}}
/>
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx
around lines 46 to 51, the Image component lacks error handling for image
loading failures. To improve user experience, add an onError handler to the
Image component that sets a fallback image or handles the error gracefully,
ensuring the UI remains visually consistent even if the original image fails to
load.

Comment on lines 75 to 103
{/* 가격 + 버튼 */}
<div className='mt-[21px] flex items-center justify-between'>
{/* 가격 */}
<p className='text-2xl font-bold text-black'>
{totalPrice.toLocaleString()}
</p>

{/* 버튼 */}
<div>
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
>
예약 취소
</Button>
)}
{showReviewButton && (
<Button
variant='primary'
className='bg-nomad h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onReview?.(id)}
>
후기 작성
</Button>
)}
</div>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

버튼 영역 구현 검토

가격 표시와 조건부 버튼 렌더링이 적절히 구현되었습니다. toLocaleString()을 사용한 가격 포맷팅도 좋습니다. 단, 두 버튼이 동시에 표시될 경우 레이아웃을 고려해야 합니다.

버튼들이 동시에 표시될 경우를 대비해 간격 조정이 필요할 수 있습니다:

          <div>
+           <div className='flex gap-2'>
            {showCancelButton && (
              <Button
                variant='secondary'
                className='h-[43px] w-[144px] rounded-md text-lg font-bold'
                onClick={() => onCancel?.(id)}
              >
                예약 취소
              </Button>
            )}
            {showReviewButton && (
              <Button
                variant='primary'
                className='bg-nomad h-[43px] w-[144px] rounded-md text-lg font-bold'
                onClick={() => onReview?.(id)}
              >
                후기 작성
              </Button>
            )}
+           </div>
          </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* 가격 + 버튼 */}
<div className='mt-[21px] flex items-center justify-between'>
{/* 가격 */}
<p className='text-2xl font-bold text-black'>
{totalPrice.toLocaleString()}
</p>
{/* 버튼 */}
<div>
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
>
예약 취소
</Button>
)}
{showReviewButton && (
<Button
variant='primary'
className='bg-nomad h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onReview?.(id)}
>
후기 작성
</Button>
)}
</div>
</div>
{/* 가격 + 버튼 */}
<div className='mt-[21px] flex items-center justify-between'>
{/* 가격 */}
<p className='text-2xl font-bold text-black'>
{totalPrice.toLocaleString()}
</p>
{/* 버튼 */}
<div>
<div className='flex gap-2'>
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
>
예약 취소
</Button>
)}
{showReviewButton && (
<Button
variant='primary'
className='bg-nomad h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onReview?.(id)}
>
후기 작성
</Button>
)}
</div>
</div>
</div>
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx
around lines 75 to 103, the price and conditional buttons are rendered
correctly, but when both cancel and review buttons appear together, their layout
spacing needs adjustment. Add appropriate horizontal spacing (e.g., margin or
gap) between the two buttons inside their container div to ensure they don't
appear too close and maintain a clean layout.

Comment on lines 84 to 92
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
>
예약 취소
</Button>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

접근성 개선 필요

버튼에 aria-label이나 더 구체적인 접근성 속성을 추가하면 스크린 리더 사용자에게 더 나은 경험을 제공할 수 있습니다.

              <Button
                variant='secondary'
                className='h-[43px] w-[144px] rounded-md text-lg font-bold'
                onClick={() => onCancel?.(id)}
+               aria-label={`${activity.title} 예약 취소`}
              >
                예약 취소
              </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
>
예약 취소
</Button>
)}
{showCancelButton && (
<Button
variant='secondary'
className='h-[43px] w-[144px] rounded-md text-lg font-bold'
onClick={() => onCancel?.(id)}
aria-label={`${activity.title} 예약 취소`}
>
예약 취소
</Button>
)}
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx
around lines 84 to 92, the cancel button lacks accessibility attributes. Add an
appropriate aria-label to the Button component that clearly describes its
action, such as "예약 취소 버튼" or a more specific label including the reservation
id, to improve screen reader usability.

Copy link
Contributor

@minimo-9 minimo-9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지 구현하시느라 고생 많으셨습니다! 안에 css만 일부 수정하시면 충분한 것 같습니다!

Comment on lines 32 to 59
{/* 체크 아이콘 */}
<div className='flex justify-center'>
<CheckIcon size={24} />
</div>

{/* 메시지 */}
<p className='text-nomad text-center text-lg font-medium'>
예약을 취소하시겠어요?
</p>

{/* 버튼 */}
<div className='flex gap-12'>
<Button
variant='secondary'
className='text-md h-[38px] w-[80px] rounded-lg border border-gray-300 font-medium'
onClick={onCancel}
disabled={isLoading}
>
아니오
</Button>
<Button
variant='primary'
className='text-md bg-nomad h-[38px] w-[80px] rounded-lg font-medium text-white'
onClick={onConfirm}
disabled={isLoading}
>
{isLoading ? '취소 중...' : '취소하기'}
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 팝업 컴포넌트 이용하시면 좋을 것 같습니다! 이미 만들어 둔 부분이라 확인 해보세요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Popup이 아닌 Modal을 선택해서 사용한 이유가 isLoading 상태 처리 및 동적 텍스트가 필요하기에 Popup에는 props에 isLoading?:boolean이 존재하지 않는 반면 Modal 컴포넌트의 경우 isLoading?:boolean이 존재하여 Modal 컴포넌트를 선택해서 사용했습니다.

const showReviewButton = isCompleted && !reviewSubmitted;

return (
<div className='flex h-[204px] w-[792px] overflow-hidden rounded-[24px] border border-gray-300 bg-white'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분은 h-204 이런 식으로 사용하셔도 됩니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다! 매번 []를 사용할 필요가 없는데 버릇이 되가지고 계속 사용하게 되네요.

<div className='w-full max-w-none lg:max-w-[792px]'>
{/* 제목과 필터 */}
<div className='mb-48 flex items-center justify-between'>
<h1 className='text-nomad text-[32px] leading-[42px] font-bold'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분은 text-3xl 쓰시면 line-height도 42로 맞춰집니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 감사합니다!

@minimo-9 minimo-9 changed the title Feature/71 예약 내역 페이지 구현(UI 및 API 연동) Feat/71 예약 내역 페이지 구현(UI 및 API 연동) Jul 30, 2025
Copy link
Contributor

@BokyungCodes BokyungCodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예약 내역 흐름도 안정적이고 상태별 조건 처리랑 구조도 깔끔해서 좋은 것 같아요!
고생하셨습니다.👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

♻️ Duplicate comments (7)
src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (2)

44-44: 반응형 디자인 개선 필요

이전 리뷰에서 지적된 고정 너비 문제가 여전히 남아있습니다. 작은 화면에서 레이아웃 문제가 발생할 수 있습니다.

-    <div className='rounded-24 flex h-204 w-792 overflow-hidden border border-gray-300 bg-white'>
+    <div className='rounded-24 flex h-204 w-full max-w-792 overflow-hidden border border-gray-300 bg-white'>

47-52: 이미지 오류 처리 추가 필요

이전 리뷰에서 지적된 이미지 로딩 실패 처리가 여전히 누락되어 있습니다.

        <Image
          src={activity.bannerImageUrl}
          alt={activity.title}
          fill
          className='rounded-l-24 object-cover'
+         onError={(e) => {
+           e.currentTarget.src = '/images/placeholder.jpg';
+         }}
        />
src/hooks/useReservationQueries.ts (2)

25-30: 페이지네이션 로직 개선 권장

이전 리뷰에서 지적된 페이지네이션 로직 문제가 여전히 남아있습니다. 정확히 10개 항목이 반환될 때만 다음 페이지가 있다고 가정하는 것은 부정확할 수 있습니다.

백엔드에서 hasNext 플래그나 메타데이터를 제공하는 방식을 고려해보세요.


48-52: 사용자 경험 개선을 위한 에러 처리 방식 변경 권장

이전 리뷰에서 지적된 alert 사용 문제가 여전히 남아있습니다. 토스트 메시지나 더 나은 UX 패턴 사용을 고려해보세요.

src/app/(with-header)/mypage/reservations/page.tsx (2)

158-176: 로딩 상태의 JSX 중복 제거 필요

로딩과 에러 상태에서 헤더 부분이 중복되어 있습니다.


170-170: 반응형 디자인 개선 필요

로딩 스켈레톤의 고정 너비가 모바일에서 문제를 일으킬 수 있습니다.

src/apis/reservations.ts (1)

18-29: 쿼리 파라미터 생성 로직 간소화 가능

조건문을 제거하고 더 간결하게 작성할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87b488d and b659067.

📒 Files selected for processing (9)
  • public/assets/svg/star-empty.tsx (1 hunks)
  • src/apis/reservations.ts (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/page.tsx (1 hunks)
  • src/app/api/reservations/[id]/reviews/route.ts (1 hunks)
  • src/components/Rating.tsx (1 hunks)
  • src/hooks/useReservationQueries.ts (1 hunks)
  • src/types/reservationTypes.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
public/assets/svg/star-empty.tsx (1)
src/types/svgType.ts (1)
  • SvgProps (1-5)
src/app/api/reservations/[id]/reviews/route.ts (6)
src/app/api/activities/[id]/reservation/route.ts (1)
  • POST (10-52)
src/app/api/addActivity/route.ts (1)
  • POST (12-69)
src/app/api/test/route.ts (1)
  • GET (9-38)
src/app/api/image/route.ts (1)
  • POST (12-65)
src/app/api/auth/refresh/route.ts (1)
  • POST (26-68)
src/app/api/users/me/route.ts (1)
  • PATCH (50-85)
src/apis/reservations.ts (2)
src/types/reservationTypes.ts (6)
  • GetMyReservationsParams (44-48)
  • MyReservationsResponse (32-36)
  • UpdateReservationRequest (39-41)
  • Reservation (10-29)
  • CreateReviewRequest (51-54)
  • ReviewResponse (57-67)
src/apis/privateInstance.ts (1)
  • privateInstance (53-53)
🪛 Biome (2.1.2)
public/assets/svg/star-empty.tsx

[error] 5-14: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

src/components/Rating.tsx

[error] 35-35: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)

src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx

[error] 65-68: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🔇 Additional comments (6)
src/app/api/reservations/[id]/reviews/route.ts (1)

11-56: API 라우트 구현이 적절합니다

다른 API 라우트와 일관된 패턴을 따르고 있으며, 인증 처리와 에러 핸들링이 적절히 구현되어 있습니다. 코드가 깔끔하고 기존 컨벤션을 잘 따르고 있습니다.

src/components/Rating.tsx (1)

14-56: 컴포넌트 구현이 잘 되어 있습니다

5점 별점 시스템이 적절히 구현되어 있고, 호버 효과와 클릭 이벤트 처리가 사용자 경험을 고려하여 잘 만들어져 있습니다. TypeScript 타입 정의도 명확합니다.

src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (2)

16-19: 타임존 처리 개선이 잘 되었습니다

이전 리뷰에서 지적된 타임존 문제가 적절히 해결되었습니다. KST 타임존(+09:00)을 명시하여 서버와 클라이언트 간 시간 차이 문제를 해결했습니다.


38-41: 조건부 렌더링 로직이 적절합니다

체험 완료 여부와 리뷰 제출 상태에 따른 버튼 표시 로직이 명확하고 사용자 요구사항을 잘 반영하고 있습니다.

src/hooks/useReservationQueries.ts (2)

11-13: 쿼리 키 관리가 잘 구현되었습니다

상수로 쿼리 키를 관리하여 일관성을 유지하고 있고, TypeScript의 const assertion을 적절히 활용하고 있습니다.


56-70: 후기 작성 훅 구현이 적절합니다

mutation 함수와 성공 시 캐시 무효화 처리가 적절히 구현되어 있습니다. 타입 안전성도 잘 유지되고 있습니다.

Comment on lines +4 to +19
const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox='0 0 20 20'
fill="none"
stroke={color}
strokeWidth="1"
{...props}
>
<path
d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z'
/>
</svg>
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

접근성 개선을 위한 title 요소 추가 필요

정적 분석 도구가 지적한 대로, SVG 요소에 대체 텍스트가 없어 스크린 리더 사용자의 접근성이 떨어집니다.

다음과 같이 title 요소를 추가하여 접근성을 개선하세요:

const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => (
  <svg
    xmlns='http://www.w3.org/2000/svg'
    width={size}
    height={size}
    viewBox='0 0 20 20'
    fill="none"
    stroke={color}
    strokeWidth="1"
    {...props}
  >
+   <title>빈 별</title>
    <path
      d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z'
    />
  </svg>
);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox='0 0 20 20'
fill="none"
stroke={color}
strokeWidth="1"
{...props}
>
<path
d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z'
/>
</svg>
);
const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox='0 0 20 20'
fill="none"
stroke={color}
strokeWidth="1"
{...props}
>
<title>빈 별</title>
<path
d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z'
/>
</svg>
);
🧰 Tools
🪛 Biome (2.1.2)

[error] 5-14: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In public/assets/svg/star-empty.tsx around lines 4 to 19, the SVG element lacks
a title element for accessibility, which reduces screen reader support. Add a
<title> element inside the SVG with a descriptive text like "Empty star icon" to
provide alternative text for screen readers and improve accessibility.

Comment on lines +15 to +64
export const getMyReservations = async (
params: GetMyReservationsParams,
): Promise<MyReservationsResponse> => {
const queryParams = new URLSearchParams();

if (params.cursorId) {
queryParams.append('cursorId', params.cursorId.toString());
}
if (params.size) {
queryParams.append('size', params.size.toString());
}
if (params.status) {
queryParams.append('status', params.status);
}

const response = await privateInstance.get(
`/reservations?${queryParams.toString()}`,
);
return response.data;
};

/**
* 내 예약 수정(취소)
* PATCH /api/reservations/{reservationId}
*/
export const updateMyReservation = async (
reservationId: number,
data: UpdateReservationRequest,
): Promise<Reservation> => {
const response = await privateInstance.patch(
`/reservations/${reservationId}`,
data,
);
return response.data;
};

/**
* 내 예약 리뷰 작성
* POST /api/reservations/{reservationId}/reviews
*/
export const createReview = async (
reservationId: number,
data: CreateReviewRequest,
): Promise<ReviewResponse> => {
const response = await privateInstance.post(
`/reservations/${reservationId}/reviews`,
data,
);
return response.data;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

API 함수에 에러 처리 추가 권장

모든 API 함수에서 에러를 catch하고 의미 있는 에러 메시지로 변환하면 좋겠습니다.

에러 처리 래퍼 함수 추가:

const handleApiError = (error: any, context: string) => {
  if (error.response?.data?.message) {
    throw new Error(error.response.data.message);
  }
  throw new Error(`${context} 중 오류가 발생했습니다.`);
};

export const getMyReservations = async (
  params: GetMyReservationsParams,
): Promise<MyReservationsResponse> => {
  try {
    // 기존 코드
  } catch (error) {
    handleApiError(error, '예약 목록 조회');
  }
};
🤖 Prompt for AI Agents
In src/apis/reservations.ts from lines 15 to 64, the API functions lack error
handling which can lead to unclear error messages. Add a helper function
handleApiError that extracts meaningful error messages from the API response or
provides a generic context-specific message. Wrap the existing API calls in
try-catch blocks and call handleApiError in the catch to throw improved error
messages for getMyReservations, updateMyReservation, and createReview functions.

Comment on lines +10 to +21
interface ReviewModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (rating: number, content: string) => void;
isLoading?: boolean;
activityTitle?: string;
activityImage?: string;
activityDate?: string;
activityTime?: string;
headCount?: number;
totalPrice?: number;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

props 구조 개선 고려

활동 정보를 개별 props로 전달하는 대신, 단일 객체로 그룹화하면 더 깔끔할 것 같습니다.

 interface ReviewModalProps {
   isOpen: boolean;
   onClose: () => void;
   onConfirm: (rating: number, content: string) => void;
   isLoading?: boolean;
-  activityTitle?: string;
-  activityImage?: string;
-  activityDate?: string;
-  activityTime?: string;
-  headCount?: number;
-  totalPrice?: number;
+  activityInfo?: {
+    title: string;
+    image: string;
+    date: string;
+    time: string;
+    headCount: number;
+    totalPrice: number;
+  };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface ReviewModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (rating: number, content: string) => void;
isLoading?: boolean;
activityTitle?: string;
activityImage?: string;
activityDate?: string;
activityTime?: string;
headCount?: number;
totalPrice?: number;
}
interface ReviewModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (rating: number, content: string) => void;
isLoading?: boolean;
activityInfo?: {
title: string;
image: string;
date: string;
time: string;
headCount: number;
totalPrice: number;
};
}
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx around
lines 10 to 21, the activity-related props are passed individually, which can
clutter the component interface. Refactor the ReviewModalProps interface to
group all activity-related properties into a single object prop, such as
activityInfo, containing title, image, date, time, headCount, and totalPrice.
Update the component and its usage accordingly to accept and use this grouped
object instead of separate props.

Comment on lines +39 to +46
if (rating === 0) {
alert('별점을 선택해주세요.');
return;
}
if (content.trim() === '') {
alert('후기를 작성해주세요.');
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

alert 대신 인라인 검증 메시지 사용 권장

폼 검증 피드백으로 alert()를 사용하는 것보다 인라인 에러 메시지가 더 나은 UX를 제공합니다.

상태로 에러 메시지를 관리하고 UI에 표시:

+  const [errors, setErrors] = useState({ rating: '', content: '' });

   const handleSubmit = () => {
+    const newErrors = { rating: '', content: '' };
+    
     if (rating === 0) {
-      alert('별점을 선택해주세요.');
+      newErrors.rating = '별점을 선택해주세요.';
-      return;
     }
     if (content.trim() === '') {
-      alert('후기를 작성해주세요.');
+      newErrors.content = '후기를 작성해주세요.';
+    }
+    
+    setErrors(newErrors);
+    if (newErrors.rating || newErrors.content) {
       return;
     }
     onConfirm(rating, content);
   };
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx around
lines 39 to 46, replace the alert() calls used for validation feedback with
inline error messages. Manage error messages using component state and display
them directly in the UI near the relevant input fields to improve user
experience.

Comment on lines +63 to +69
<button
onClick={handleClose}
className='text-gray-400 hover:text-gray-600'
disabled={isLoading}
>
<Close size={24} color='currentColor' />
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

버튼에 명시적 type 속성 추가 필요

닫기 버튼에 type="button"을 추가하여 폼 제출을 방지해야 합니다.

             <button
               onClick={handleClose}
               className='text-gray-400 hover:text-gray-600'
               disabled={isLoading}
+              type="button"
             >
               <Close size={24} color='currentColor' />
             </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={handleClose}
className='text-gray-400 hover:text-gray-600'
disabled={isLoading}
>
<Close size={24} color='currentColor' />
</button>
<button
onClick={handleClose}
className='text-gray-400 hover:text-gray-600'
disabled={isLoading}
type="button"
>
<Close size={24} color='currentColor' />
</button>
🧰 Tools
🪛 Biome (2.1.2)

[error] 65-68: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx around
lines 63 to 69, the close button lacks an explicit type attribute, which can
cause it to act as a submit button inside a form. Add type="button" to the
button element to prevent unintended form submission when it is clicked.

Comment on lines +117 to +133
onSuccess: () => {
alert('후기가 작성되었습니다.');
setReviewModal({
isOpen: false,
reservationId: null,
activityTitle: null,
activityImage: null,
activityDate: null,
activityTime: null,
headCount: null,
totalPrice: null,
});
},
onError: (error) => {
alert(`후기 작성 실패: ${error.message}`);
},
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

alert 대신 토스트 메시지 사용 권장

사용자 피드백을 위해 alert()을 사용하는 것은 UX에 좋지 않습니다. 토스트 메시지나 커스텀 알림 컴포넌트를 사용하는 것이 좋습니다.

토스트 라이브러리를 사용하여 개선:

-            alert('후기가 작성되었습니다.');
+            toast.success('후기가 작성되었습니다.');

-            alert(`후기 작성 실패: ${error.message}`);
+            toast.error(`후기 작성 실패: ${error.message}`);
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/page.tsx around lines 117 to 133,
replace the use of alert() for success and error messages with a toast
notification system. Import and use a toast library or custom toast component to
show non-blocking, user-friendly messages instead of alert dialogs. Update the
onSuccess and onError handlers to trigger toast notifications with appropriate
success or error messages.

{/* TODO: 예약 내역 컴포넌트 구현 */}
{/* 무한 스크롤 로딩 */}
{isFetchingNextPage && (
<div className='rounded-24 h-204 w-792 animate-pulse bg-gray-200' />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

무한 스크롤 로딩 스켈레톤도 반응형으로 수정 필요

무한 스크롤 로딩 인디케이터도 고정 너비를 사용하고 있습니다.

-              <div className='rounded-24 h-204 w-792 animate-pulse bg-gray-200' />
+              <div className='rounded-24 h-204 w-full max-w-[792px] animate-pulse bg-gray-200' />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className='rounded-24 h-204 w-792 animate-pulse bg-gray-200' />
<div className='rounded-24 h-204 w-full max-w-[792px] animate-pulse bg-gray-200' />
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/page.tsx at line 229, the infinite
scroll loading skeleton uses a fixed width class 'w-792', which is not
responsive. Replace the fixed width with responsive width classes or use
relative units like percentages or max-width to ensure the loading skeleton
adapts to different screen sizes.

};

return (
<div className={`flex gap-8 ${className}`} onMouseLeave={handleMouseLeave}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

접근성 개선을 위한 role 속성 추가 필요

정적 분석 도구가 지적한 대로, 마우스 이벤트가 있는 div 요소에 적절한 role 속성이 없어 접근성이 떨어집니다.

다음과 같이 role 속성을 추가하여 접근성을 개선하세요:

-    <div className={`flex gap-8 ${className}`} onMouseLeave={handleMouseLeave}>
+    <div className={`flex gap-8 ${className}`} onMouseLeave={handleMouseLeave} role="radiogroup" aria-label="별점 선택">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className={`flex gap-8 ${className}`} onMouseLeave={handleMouseLeave}>
<div
className={`flex gap-8 ${className}`}
onMouseLeave={handleMouseLeave}
role="radiogroup"
aria-label="별점 선택"
>
🧰 Tools
🪛 Biome (2.1.2)

[error] 35-35: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)

🤖 Prompt for AI Agents
In src/components/Rating.tsx at line 35, the div element with mouse event
handlers lacks a role attribute, which reduces accessibility. Add an appropriate
role attribute, such as role="button" or another suitable role depending on the
element's function, to improve accessibility and satisfy static analysis tools.

Comment on lines +24 to +28
date: string;
startTime: string;
endTime: string;
createdAt: string;
updatedAt: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

날짜/시간 필드 타입 개선 제안

날짜와 시간 필드가 string으로 정의되어 있는데, 더 명확한 타입이나 포맷 검증이 있으면 좋겠습니다.

ISO 8601 형식을 명시하거나 branded type 사용:

type ISODateString = string & { __brand: 'ISODate' };
type TimeString = string & { __brand: 'Time' }; // HH:MM format

export interface Reservation {
  // ...
  date: ISODateString;
  startTime: TimeString;
  endTime: TimeString;
  createdAt: ISODateString;
  updatedAt: ISODateString;
}
🤖 Prompt for AI Agents
In src/types/reservationTypes.ts around lines 24 to 28, the date and time fields
are currently typed as plain strings, which lacks clarity and validation. Define
branded types for ISODateString and TimeString to represent ISO 8601 date
strings and HH:MM time strings respectively, then update the Reservation
interface to use these branded types for date, startTime, endTime, createdAt,
and updatedAt fields to improve type safety and expressiveness.

Comment on lines +39 to +41
export interface UpdateReservationRequest {
status: 'canceled';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

UpdateReservationRequest 확장성 고려

현재는 'canceled' 상태만 허용하는데, 향후 다른 상태 변경도 필요할 수 있습니다.

API 설계 시 확장성을 고려하여 다음과 같이 개선할 수 있습니다:

export interface UpdateReservationRequest {
  status: Extract<ReservationStatus, 'canceled' | 'confirmed'>;
  reason?: string; // 취소 사유 등 추가 정보
}
🤖 Prompt for AI Agents
In src/types/reservationTypes.ts around lines 39 to 41, the
UpdateReservationRequest interface currently only allows the status 'canceled',
limiting future extensibility. Modify the status property to accept multiple
relevant statuses such as 'canceled' and 'confirmed' using a type like
Extract<ReservationStatus, 'canceled' | 'confirmed'>, and add an optional reason
field to provide additional context like cancellation reasons.

@LeeCh0129 LeeCh0129 changed the title Feat/71 예약 내역 페이지 구현(UI 및 API 연동) Feat/71 예약 내역 페이지, 후기 작성 구현(UI 및 API 연동) Jul 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx (1)

22-30: 스타일링 중복 및 클래스 명명 불일치 문제

이전 리뷰에서 지적된 중복 스타일링 문제가 여전히 존재하며, 추가로 Tailwind 클래스 명명이 일관되지 않습니다 (!w-298px vs !h-184).

Line 22에서 !h-184!h-184px로 수정해야 합니다.

-      <Modal.Content className='!w-298px !h-184 !max-w-none !min-w-0 !rounded-xl !p-0'>
+      <Modal.Content className='!w-298px !h-184px !max-w-none !min-w-0 !rounded-xl !p-0 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'>
         <div
-          className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16'
-          style={{
-            borderRadius: '12px',
-            background: '#FFFFFF',
-            boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)',
-            overflow: 'hidden',
-          }}
+          className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16 overflow-hidden'
         >
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b659067 and e808e82.

📒 Files selected for processing (2)
  • src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx (1 hunks)
  • src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx (1 hunks)
🔇 Additional comments (6)
src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx (3)

3-17: 컴포넌트 구현이 깔끔합니다.

빈 상태를 표시하는 컴포넌트로서 적절한 구조와 스타일링을 가지고 있습니다. Flexbox를 사용한 중앙 정렬과 적절한 여백 설정이 잘 되어 있습니다.


12-14: 메시지 텍스트 검토 필요.

"아직 등록한 체험이 없어요"라는 메시지가 예약 내역 페이지의 빈 상태에 적합한지 확인이 필요합니다. 예약 내역이 없는 상황에서는 "예약한 체험이 없어요" 또는 "예약 내역이 없어요"가 더 정확할 수 있습니다.

PR 목적과 사용자 시나리오를 고려하여 메시지 텍스트가 적절한지 검토해 주세요. 이 컴포넌트가 예약 내역이 없을 때 표시되는 것이므로, "등록한 체험"보다는 "예약한 체험"이 더 맞는 표현일 수 있습니다.


1-1: import 경로 확인 완료: 올바른 설정 확인

tsconfig.json에 "@assets/*": ["./public/assets/*"]가 설정되어 있어
@assets/svg/empty-documentpublic/assets/svg/empty-document.tsx를 정확히 가리킵니다.
따라서 import 구문을 그대로 유지하시면 됩니다.

src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx (3)

1-12: 인터페이스 정의가 명확하고 잘 구조화되어 있습니다.

TypeScript 타입 정의가 적절하고, optional 프로퍼티 처리도 올바르게 되어 있습니다.


32-40: 아이콘과 메시지 구현이 적절합니다.

CheckIcon 사용과 한국어 메시지가 사용자 경험에 적합하게 구현되어 있습니다.


21-21: Modal 사용 패턴이 적절합니다.

Modal 컴포넌트 선택에 대한 개발자의 설명이 합리적이며, onOpenChange 콜백 처리도 올바르게 구현되어 있습니다.

Comment on lines +44 to +59
<Button
variant='secondary'
className='text-md h-38px w-80px rounded-lg border border-gray-300 font-medium'
onClick={onCancel}
disabled={isLoading}
>
아니오
</Button>
<Button
variant='primary'
className='text-md bg-nomad h-38 w-80 rounded-lg font-medium text-white'
onClick={onConfirm}
disabled={isLoading}
>
{isLoading ? '취소 중...' : '취소하기'}
</Button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

버튼 스타일링 불일치 및 접근성 개선 필요

  1. 클래스 명명이 일관되지 않습니다 (h-38px vs h-38, w-80px vs w-80)
  2. 로딩 상태에서 접근성을 위한 aria-label이 부족합니다
  3. Primary 버튼에 중복 스타일이 있습니다
           <Button
             variant='secondary'
-            className='text-md h-38px w-80px rounded-lg border border-gray-300 font-medium'
+            className='text-md h-38px w-80px rounded-lg border border-gray-300 font-medium'
             onClick={onCancel}
             disabled={isLoading}
+            aria-label='예약 취소 거부'
           >
             아니오
           </Button>
           <Button
             variant='primary'
-            className='text-md bg-nomad h-38 w-80 rounded-lg font-medium text-white'
+            className='text-md h-38px w-80px rounded-lg font-medium'
             onClick={onConfirm}
             disabled={isLoading}
+            aria-label={isLoading ? '예약 취소 처리 중' : '예약 취소 확인'}
           >
             {isLoading ? '취소 중...' : '취소하기'}
           </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
variant='secondary'
className='text-md h-38px w-80px rounded-lg border border-gray-300 font-medium'
onClick={onCancel}
disabled={isLoading}
>
아니오
</Button>
<Button
variant='primary'
className='text-md bg-nomad h-38 w-80 rounded-lg font-medium text-white'
onClick={onConfirm}
disabled={isLoading}
>
{isLoading ? '취소 중...' : '취소하기'}
</Button>
<Button
variant='secondary'
className='text-md h-38px w-80px rounded-lg border border-gray-300 font-medium'
onClick={onCancel}
disabled={isLoading}
aria-label='예약 취소 거부'
>
아니오
</Button>
<Button
variant='primary'
className='text-md h-38px w-80px rounded-lg font-medium'
onClick={onConfirm}
disabled={isLoading}
aria-label={isLoading ? '예약 취소 처리 중' : '예약 취소 확인'}
>
{isLoading ? '취소 중...' : '취소하기'}
</Button>
🤖 Prompt for AI Agents
In
src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx
between lines 44 and 59, fix the button styling inconsistencies by using
consistent class naming conventions (e.g., use either h-38 or h-38px uniformly
for height and w-80 or w-80px for width). Add appropriate aria-label attributes
to the buttons to improve accessibility, especially reflecting the loading state
on the primary button. Remove redundant or duplicate styles from the primary
button's className to keep the styling clean and maintainable.


export default function EmptyReservations() {
return (
<div className='flex flex-col items-center justify-center py-120'>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

접근성 개선 제안.

현재 구현은 기능적으로 올바르지만, 접근성을 위해 의미적 HTML 구조 개선을 고려해보세요.

다음과 같이 의미적 구조로 개선할 수 있습니다:

-    <div className='flex flex-col items-center justify-center py-120'>
+    <section className='flex flex-col items-center justify-center py-120' role="status" aria-label="예약 내역 없음">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className='flex flex-col items-center justify-center py-120'>
<section className='flex flex-col items-center justify-center py-120' role="status" aria-label="예약 내역 없음">
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx at
line 5, the current div used for layout lacks semantic HTML elements which can
improve accessibility. Replace or wrap the div with more meaningful semantic
tags such as <main>, <section>, or <article> depending on the context, and
ensure appropriate ARIA roles or landmarks are used to enhance screen reader
navigation and overall accessibility.

<div className='flex flex-col items-center justify-center py-120'>
{/* 빈 상태 아이콘 */}
<div className='mb-24'>
<EmptyDocumentIcon size={131} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

아이콘 크기 상수화 제안.

하드코딩된 아이콘 크기(131)를 상수로 분리하면 유지보수성이 향상됩니다.

다음과 같이 상수로 분리할 수 있습니다:

+const EMPTY_ICON_SIZE = 131;
+
 export default function EmptyReservations() {
   return (
     <div className='flex flex-col items-center justify-center py-120'>
       {/* 빈 상태 아이콘 */}
       <div className='mb-24'>
-        <EmptyDocumentIcon size={131} />
+        <EmptyDocumentIcon size={EMPTY_ICON_SIZE} />
       </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<EmptyDocumentIcon size={131} />
const EMPTY_ICON_SIZE = 131;
export default function EmptyReservations() {
return (
<div className='flex flex-col items-center justify-center py-120'>
{/* 빈 상태 아이콘 */}
<div className='mb-24'>
<EmptyDocumentIcon size={EMPTY_ICON_SIZE} />
</div>
{/* …rest of the component */}
</div>
);
}
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/reservations/components/EmptyReservations.tsx at
line 8, the icon size is hardcoded as 131. To improve maintainability, define a
constant for the icon size at the top of the file and replace the hardcoded
value with this constant in the EmptyDocumentIcon component.

@LeeCh0129 LeeCh0129 merged commit 36a4292 into develop Jul 31, 2025
2 checks passed
@LeeCh0129 LeeCh0129 deleted the feat/71 branch July 31, 2025 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 후기 작성 컴포넌트 및 API연동 [feat] 예약 내역 API 연결 [feat] 예약 내역 페이지 레이아웃

4 participants